web-six-seven (MCTF)
Challenge
The challenge provided a single file named 67, which is a stripped Linux ELF binary.
The binary expected one command-line argument (./vm <flag>), rejected most lengths, and validated input using a custom VM-like routine. The valid input turned out to be a full Rick Astley lyrics block, ending with the real CTF flag.
Solution
Summary
Final flag:
MCTF{R1ck_R0ll3d_1s_N0t_a_Pr0blem_1s_iT?}
1. Initial Recon
File identification
file 67-> ELF 64-bit LSB executable, stripped, dynamically linked.
Basic runtime behavior
Running in Linux (amd64) showed:
- No args:
Usage: ./vm <flag> - Random arg:
Taille invalide.orEchec. - Success message present in strings:
Flag Valid/Flag Valide.
Because the host OS was macOS, the binary was executed inside Docker with:
--platform linux/amd64
2. Length Constraint Discovery
By fuzzing argument lengths, only lengths divisible by 3 were accepted for deeper checking:
3, 6, 9, ...->Echec.- Other lengths ->
Taille invalide.
This suggested processing input in 3-byte chunks (24-bit blocks).
3. Reverse Engineering the Checker
Disassembly around main checker (.text around 0x1200) showed:
- A state variable initialized to
ebx = 0xDEADBEEF. - A bytecode stream starting at
.data + 0x20(VA0x4020, file offset0x3020). - Opcodes decoded as
decoded = byte ^ 0x7A. - Dispatch table at
.rodata + 0x4(VA0x2004) with handlers for:j(op 0): load next 3-byte input blockk(op 1):x ^= x << 13h(op 2):x ^= x >> 17i(op 3):x ^= x << 5n(op 4):x *= 0x2545F491o(op 5):ebx = ecx ^ ebp(compare/finalize relation)Z(op 16): compare against embedded 32-bit immediate
- Stream terminator byte:
0x85.
Program structure extracted from bytecode:
- 287 rounds total.
- Each round loads one 3-byte block.
- Each round applies exactly 1000 loop steps of
k,h,i,n. - Each round ends in
othenZ <imm32>compare.
Therefore required input length:
287 * 3 = 861bytes.
4. Why SMT Was Abandoned
A direct SMT approach with Z3 for one round returned unknown (timeout) even with printable-byte constraints.
Given the function is deterministic over a 24-bit block space, brute force per round was more practical in native code.
5. Native Solver Strategy
A C++ solver was built (solve_vm67_native.cpp) with:
- Exact VM round emulation.
- Extraction of all 287 target constants from encoded bytecode.
- Multithreaded brute force over
2^24candidates for each round. - Chaining state (
prev = target_of_previous_round).
Performance was good enough to complete all rounds and recover all 861 bytes.
Recovered output was saved to:
recovered_flag.txtrecovered_flag.bin
6. Validation
Passing recovered bytes back as argv in Linux (using Python subprocess to preserve exact bytes/newlines):
- length: 861
- return code: 0
- output:
Flag Valide.
So recovery was correct.
7. Recovered Payload
The recovered input is a Rickroll lyrics block ending with:
Good Job, the flag is : MCTF{R1ck_R0ll3d_1s_N0t_a_Pr0blem_1s_iT?}
8. Files Produced During Solve
bench_round.c(single-round brute-force benchmark)solve_vm67.py(initial symbolic/SMT exploration)solve_vm67_native.cpp(final full solver)recovered_flag.txt(full recovered valid argument)recovered_flag.bin(binary form)
Final flag:
MCTF{R1ck_R0ll3d_1s_N0t_a_Pr0blem_1s_iT?}